﻿#region License, Terms and Conditions
//
// Jayrock - A JSON-RPC implementation for the Microsoft .NET Framework
// Written by Atif Aziz (www.raboof.com)
// Copyright (c) Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

#if !NET_1_0 && !NET_1_1

namespace Jayrock.GeoJson
{
    #region Imports

    using System;
    using System.Collections.Generic;
    using Jayrock.Json;

    #endregion

    /// <summary>
    /// Provides members to read the <c>coordinates</c> member 
    /// value of various <a href="http://geojson.org/geojson-spec.html#geometry-objects">GeoJSON geometry objects</a>.
    /// </summary>

    public static class GeoJsonCoordsReader
    {
        /// <summary>
        /// Reads three JSON numbers representing x, y and z as a 
        /// <see cref="GeoJsonPosition"/>. The z component is
        /// optional and initialized to <see cref="double.NaN"/>
        /// if absent.
        /// </summary>

        // ReSharper disable InconsistentNaming
        public static GeoJsonPosition ReadXYZ(JsonReader reader)
        // ReSharper restore InconsistentNaming
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            double x = reader.ReadNumber().ToDouble();
            double y = reader.ReadNumber().ToDouble();
            double z = reader.TokenClass == JsonTokenClass.Number
                     ? reader.ReadNumber().ToDouble()
                     : Double.NaN;

            return new GeoJsonPosition(x, y, z);
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#point">Point</a> 
        /// as a single <see cref="GeoJsonPosition"/>.
        /// </summary>

        public static GeoJsonPosition ReadPoint(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // 2.1.2. Point
            // 
            // For type "Point", the "coordinates" member must be a single 
            // position.

            reader.ReadToken(JsonTokenClass.Array);
            GeoJsonPosition result = ReadXYZ(reader);
            reader.ReadToken(JsonTokenClass.EndArray);
            return result;
        }

        /// <summary>
        /// Same as <see cref="ReadMultiPoint(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<GeoJsonPosition> ReadMultiPoint(JsonBuffer buffer)
        {
            return ReadMultiPoint(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#multipoint">MultiPoint</a> 
        /// as a sequence of <see cref="GeoJsonPosition"/> items.
        /// </summary>

        public static IEnumerable<GeoJsonPosition> ReadMultiPoint(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // TODO Make argument checking eager while iteration lazy

            // 2.1.3. MultiPoint
            //
            // For type "MultiPoint", the "coordinates" member must be an 
            // array of positions.

            reader.ReadToken(JsonTokenClass.Array);
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return ReadPoint(reader);
            reader.ReadToken(JsonTokenClass.EndArray);
        }

        /// <summary>
        /// Same as <see cref="ReadLineString(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<GeoJsonPosition> ReadLineString(JsonBuffer buffer)
        {
            return ReadLineString(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#linestring">LineString</a> 
        /// as a sequence of (minimum two) <see cref="GeoJsonPosition"/> items.
        /// </summary>
        /// <remarks>
        /// This method uses deferred execution and non-repeatable semantics.
        /// </remarks>

        public static IEnumerable<GeoJsonPosition> ReadLineString(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // TODO Make argument checking eager while iteration lazy

            // 2.1.4. LineString
            //
            // For type "LineString", the "coordinates" member must be an 
            // array of two or more positions.

            reader.ReadToken(JsonTokenClass.Array);
            yield return ReadPoint(reader);
            yield return ReadPoint(reader);
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return ReadPoint(reader);
            reader.ReadToken(JsonTokenClass.EndArray);
        }

        /// <summary>
        /// Same as <see cref="ReadMultiLineString(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<IEnumerable<GeoJsonPosition>> ReadMultiLineString(JsonBuffer buffer)
        {
            return ReadMultiLineString(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#multilinestring">MultiLineString</a> 
        /// as a sequence of <see cref="GeoJsonPosition"/> sequences.
        /// </summary>
        /// <remarks>
        /// This method uses deferred execution semantics and where the 
        /// outer most sequence is non-repeatable.
        /// </remarks>

        public static IEnumerable<IEnumerable<GeoJsonPosition>> ReadMultiLineString(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // TODO Make argument checking eager while iteration lazy

            // 2.1.5. MultiLineString
            //
            // For type "MultiLineString", the "coordinates" member must 
            // be an array of LineString coordinate arrays.

            reader.ReadToken(JsonTokenClass.Array);
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return Array(ReadLineString(reader));
            reader.ReadToken(JsonTokenClass.EndArray);
        }

        /// <summary>
        /// Same as <see cref="ReadLinearRing(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<GeoJsonPosition> ReadLinearRing(JsonBuffer buffer)
        {
            return ReadLinearRing(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#linestring">LinearRing</a> 
        /// as a sequence of at least four <see cref="GeoJsonPosition"/> items
        /// and where the first and last <see cref="GeoJsonPosition"/> items are
        /// equivalent.
        /// </summary>
        /// <remarks>
        /// This method uses deferred execution semantics and where the 
        /// outer most sequence is non-repeatable.
        /// </remarks>

        public static IEnumerable<GeoJsonPosition> ReadLinearRing(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // TODO Make argument checking eager while iteration lazy

            // 2.1.4. LineString
            //
            // For type "LineString", the "coordinates" member must be an 
            // array of two or more positions.
            //
            // A LinearRing is closed LineString with 4 or more positions. 
            // The first and last positions are equivalent (they represent 
            // equivalent points). Though a LinearRing is not explicitly 
            // represented as a GeoJSON geometry type, it is referred to 
            // in the Polygon geometry type definition.

            IEnumerable<GeoJsonPosition> points = ReadLineString(reader);
            using (IEnumerator<GeoJsonPosition> point = points.GetEnumerator())
            {
                GeoJsonPosition first = ReadLineStringPoint(point);
                yield return first;
                yield return ReadLineStringPoint(point);
                yield return ReadLineStringPoint(point);
                GeoJsonPosition last = ReadLineStringPoint(point);

                while (point.MoveNext())
                {
                    yield return last;
                    last = point.Current;
                }

                if (first != last)
                {
                    throw new GeoJsonException(String.Format(
                        @"First ({0}) and last ({1}) position of LinearRing are different.",
                        first, last));
                }

                yield return last;
            }
        }

        private static TPoint ReadLineStringPoint<TPoint>(IEnumerator<TPoint> enumerator)
        {
            if (!enumerator.MoveNext())
                throw new Exception("LinearRing must have at least 4 positions.");
            return enumerator.Current;
        }

        /// <summary>
        /// Same as <see cref="ReadPolygon(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<IEnumerable<GeoJsonPosition>> ReadPolygon(JsonBuffer buffer)
        {
            return ReadPolygon(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#polygon">Polygon</a> 
        /// as a sequence of <see cref="GeoJsonPosition"/> sequences and where
        /// the inner sequence represents <a href="http://geojson.org/geojson-spec.html#linestring">LinearRing</a> 
        /// coordinates.
        /// </summary>
        /// <remarks>
        /// This method uses deferred execution semantics and where the 
        /// outer most sequence is non-repeatable.
        /// </remarks>

        public static IEnumerable<IEnumerable<GeoJsonPosition>> ReadPolygon(
            JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // TODO Make argument checking eager while iteration lazy

            // 2.1.6. Polygon
            //
            // For type "Polygon", the "coordinates" member must be an 
            // array of LinearRing coordinate arrays. For Polygons with 
            // multiple rings, the first must be the exterior ring and 
            // any others must be interior rings or holes.

            reader.ReadToken(JsonTokenClass.Array);
            yield return /* exterior */ Array(ReadLinearRing(reader));
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return /* hole */ Array(ReadLinearRing(reader));
            reader.ReadToken(JsonTokenClass.EndArray);
        }

        /// <summary>
        /// Same as <see cref="ReadMultiPolygon(Jayrock.Json.JsonReader)"/> except
        /// the returned sequence can be repeatably enumerated.
        /// </summary>

        public static IEnumerable<IEnumerable<IEnumerable<GeoJsonPosition>>> ReadMultiPolygon(JsonBuffer buffer)
        {
            return ReadMultiPolygon(buffer.CreateReader());
        }

        /// <summary>
        /// Reads the coordinates of a GeoJSON 
        /// <a href="http://geojson.org/geojson-spec.html#multipolygon">MultiPolygon</a> 
        /// as a sequence of polygons where each polygon is itself a sequence of 
        /// <see cref="GeoJsonPosition"/> items representing 
        /// <a href="http://geojson.org/geojson-spec.html#linestring">LinearRing</a> 
        /// coordinates.
        /// </summary>
        /// <remarks>
        /// This method uses deferred execution semantics and where the 
        /// outer most sequence is non-repeatable.
        /// </remarks>

        public static IEnumerable<IEnumerable<IEnumerable<GeoJsonPosition>>> ReadMultiPolygon(JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader", "A valid JSON reader object is required.");

            // 2.1.7. MultiPolygon
            //
            // For type "MultiPolygon", the "coordinates" member must be 
            // an array of Polygon coordinate arrays.

            reader.ReadToken(JsonTokenClass.Array);
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return Array(ReadPolygon(reader));
            reader.ReadToken(JsonTokenClass.EndArray);
        }

        private static T[] Array<T>(IEnumerable<T> source)
        {
            using (IEnumerator<T> e = source.GetEnumerator())
            {
                //
                // Optimize for 0 to 4 elements...
                //

                if (!e.MoveNext())
                    return new T[0];

                T item1 = e.Current;
                if (!e.MoveNext())
                    return new T[] { item1 };

                T item2 = e.Current;
                if (!e.MoveNext())
                    return new T[] { item1, item2 };

                T item3 = e.Current;
                if (!e.MoveNext())
                    return new T[] { item1, item2, item3 };

                T item4 = e.Current;
                if (!e.MoveNext())
                    return new T[] { item1, item2, item3, item4 };

                //
                // ...but then generalize from 5 elements and on.
                //
                
                List<T> list = new List<T>();
                list.Add(item1);
                list.Add(item2);
                list.Add(item3);
                list.Add(item4);

                do { list.Add(e.Current); } while (e.MoveNext());
                
                return list.ToArray();
            }
        }

        internal static object ReadCoords(GeoJsonObjectType type, JsonReader reader)
        {
            switch (type)
            {
                case GeoJsonObjectType.Point: return ReadPoint(reader);
                case GeoJsonObjectType.MultiPoint: return Array(ReadMultiPoint(reader));
                case GeoJsonObjectType.LineString: return Array(ReadLineString(reader));
                case GeoJsonObjectType.MultiLineString: return Array(ReadMultiLineString(reader));
                case GeoJsonObjectType.Polygon: return Array(ReadPolygon(reader));
                case GeoJsonObjectType.MultiPolygon: return Array(ReadMultiPolygon(reader));
                default: throw new ArgumentOutOfRangeException("type", type, null);
            }
        }
    }
}

#endif // !NET_1_0 && !NET_1_1
